Mixin

注意

这是一个进阶的话题。需要建立在了解 基于类的视图的基础上。

Django的基于类的视图提供了许多功能,但是你可能只想使用其中的一部分。例如,你想编写一个视图,它渲染模板来响应HTTP,但是你用不了TemplateView;或者你只需要对POST 请求渲染一个模板,而GET 请求做一些其它的事情。 虽然你可以直接使用TemplateResponse,但是这将导致重复的代码。

由于这些原因,Django 提供许多Mixin,它们提供更细致的功能。例如,渲染模板封装在TemplateResponseMixin 中。Django 参考手册包含所有Mixin 的完整文档

Context 和TemplateResponse

在基于类的视图中使用模板具有一致的接口,有两个Mixin 起了核心的作用。

TemplateResponseMixin

返回TemplateResponse 的每个视图都将调用render_to_response() 方法,这个方法由 TemplateResponseMixin 提供。大部分时候,这个方法会隐式调用(例如,它会被TemplateViewDetailView 中实现的get() 方法调用);如果你不想通过Django 的模板渲染响应,那么你可以覆盖它,虽然通常不需要这样。其示例用法请参见JSONResponseMixin 示例

render_to_response() 本身调用get_template_names(),它默认查找类视图的template_name; 其它两个Mixin(SingleObjectTemplateResponseMixinMultipleObjectTemplateResponseMixin)覆盖了这个方法,以在处理实际的对象时能提供更灵活的默认行为。

ContextMixin

需要Context 数据的每个内建视图,例如渲染模板的视图(包括TemplateResponseMixin ),都应该以关键字参数调用get_context_data(),以确保它们想要的数据在里面。get_context_data() 返回一个字典;在ContextMixin 中,它只是简单地返回它的关键字参数,通常会覆盖这个方法来向字典中添加更多的成员。

构建Django 的基于类的通用视图函数

让我们看下Django 的两个通用的基于类的视图是如何通过互不相关的Mixin 构建的。我们将考虑DetailView,它渲染一个对象的“详细”视图,和ListView,它渲染一个对象列表,通常来自一个查询集,需要时还会分页。这将会向我们接收四个Mixin,这些Mixin 在用到单个或多个Django对象时非常有用。

在通用的编辑视图(FormView 和模型相关的视图CreateViewUpdateViewDeleteView)和基于日期的通用视图中都会涉及到Minxin。它们在Mixin 参考文档中讲述。

DetailView:用于单个Django 对象

为了显示一个对象的详细信息,我们通常需要做两件事情:查询对象然后利用合适的模板和包含该对象的Context 生成TemplateResponse

为了获得对象,DetailView 依赖SingleObjectMixin,它提供一个get_object() 方法,这个方法基于请求的URL 获取对象(它查找URLconf 中声明的pkslug 关键字参数,然后从视图的model 属性或queryset 属性查询对象)。SingleObjectMixin 还覆盖get_context_data(),这个方法在Django 所有的内建的基于类的视图中都有用到,用来给模板的渲染提供Context 数据。

然后,为了生成TemplateResponseDetailView 使用SingleObjectTemplateResponseMixin,它扩展自TemplateResponseMixin并覆盖上文讨论过的get_template_names()。实际上,它提供比较复杂的选项集合,但是大部分人用到的主要的一个是 <app_label>/<model_name>_detail.html_detail 部分可以通过设置子类的template_name_suffix 来改变。(例如,通用的编辑视图 使用_form 来创建和更新视图,用_confirm_delete 来删除视图)。

ListView:用于多个Django 对象

显示对象的列表和上面的步骤大体相同:我们需要一个对象的列表(可能是分页形式的),这通常是一个QuerySet,然后我们需要利用合适的模板和对象列表生成一个TemplateResponse

为了获取对象,ListView 使用MultipleObjectMixin,它提供get_queryset()paginate_queryset() 两种方法。与SingleObjectMixin 不同,不需要根据URL 中关键字参数来获得查询集,默认将使用视图类的querysetmodel 属性。通常需要覆盖get_queryset() 以动态获取不同的对象,例如根据当前的用户或排除打算在将来提交的博客。

MultipleObjectMixin 还覆盖get_context_data() 以包含合适的Context 变量用于分页(如果禁止分页,则提供一些假的)。这个方法依赖传递给它的关键字参数object_listListView 会负责准备好这个参数。

为了生成TemplateResponseListView 然后使用MultipleObjectTemplateResponseMixin;与上面的SingleObjectTemplateResponseMixin 类似,它覆盖get_template_names() 来提供一系列的选项,而最常用到的是<app_label>/<model_name>_list.html,其中_list 部分同样由template_name_suffix 属性设置。(基于日期的通用视图使用_archive_archive_year 等等这样的后缀来针对各种基于日期的列表视图使用不同的模板)。

使用Django 的基于类的视图的Mixin

既然我们已经看到Django 通用的基于类的视图时如何使用Mixin,让我们在看看其它组合它们的方式。当然,我们仍将它们与内建的基于类的视图或其它通用的基于类的视图组合,但是对于Django 提供的便利性你将解决一些更加罕见的问题。

警告

不是所有的Mixin 都可以一起使用,也不是所有的基于类的视图都可以与其它Mixin 一起使用。这里我们展示的是可以工作的几个例子;如果你需要其它功能,那么你必须考虑不同类之间的属性和方法的相互作用,以及方法解析顺序将如何影响方法调用的版本和顺序。

Django 的基于类的视图基于类的视图的Mixin 的文档将帮助你理解在不同的类和Mixin 之间那些属性和方法可能引起冲突。

如果有担心,通常最好退避并基于ViewTemplateView,或者可能的话加上SingleObjectMixinMultipleObjectMixin。虽然你可能最终会编写更多的代码,但是对于后来的人更容易理解,而且你自己也少了份担心。(当然,你始终可以深入探究Django 中基于类的通用视图的具体实现以获取如何处理出现的问题的灵感)。

SingleObjectMixin 与View 一起使用

如果你想编写一个简单的基于类的视图,它只响应POST,我们将子类化View 并在子类中只编写一个post() 方法。但是,如果我们想处理一个由URL 标识的特定对象,我们将需要SingleObjectMixin 提供的功能。

我们将使用在通用的基于类的视图简介 中用到的Author 模型做演示。

views.py

  1. from django.http import HttpResponseForbidden, HttpResponseRedirect
  2. from django.core.urlresolvers import reverse
  3. from django.views.generic import View
  4. from django.views.generic.detail import SingleObjectMixin
  5. from books.models import Author
  6. class RecordInterest(SingleObjectMixin, View):
  7. """Records the current user's interest in an author."""
  8. model = Author
  9. def post(self, request, *args, **kwargs):
  10. if not request.user.is_authenticated():
  11. return HttpResponseForbidden()
  12. # Look up the author we're interested in.
  13. self.object = self.get_object()
  14. # Actually record interest somehow here!
  15. return HttpResponseRedirect(reverse('author-detail', kwargs={'pk': self.object.pk}))

实际应用中,你的对象可能以键-值的方式保存而不是保存在关系数据库中,所以我们不考虑这点。使用SingleObjectMixin 的视图唯一需要担心的是在哪里查询我们感兴趣的Author,而它会用一个简单的self.get_object() 调用实现。其它的所有事情都有该Mixin 帮我们处理。

我们可以将它这样放入URL 中,非常简单:

urls.py

  1. from django.conf.urls import url
  2. from books.views import RecordInterest
  3. urlpatterns = [
  4. #...
  5. url(r'^author/(?P<pk>[0-9]+)/interest//span>, RecordInterest.as_view(), name='author-interest'),
  6. ]

注意pk 命名组,get_object() 将用它来查询Author 实例。你还可以使用slug,或者SingleObjectMixin 的其它功能。

SingleObjectMixin 与ListView 一起使用

ListView 提供内建的分页,但是可能你分页的列表中每个对象都与另外一个对象(通过一个外键)关联。在我们的Publishing 例子中,你可能想通过一个特定的Publisher 分页所有的Book。

一种方法是组合ListViewSingleObjectMixin,这样分页的Book 列表的查询集能够与找到的单个Publisher 对象关联。为了实现这点,我们需要两个不同的查询集:

Book queryset for use by ListView

Since we have access to the Publisher whose books we want to list, we simply override get_queryset() and use the Publisher’s reverse foreign key manager.

Publisher queryset for use in get_object()

We’ll rely on the default implementation of get_object() to fetch the correct Publisher object. However, we need to explicitly pass a queryset argument because otherwise the default implementation of get_object() would call get_queryset() which we have overridden to return Book objects instead of Publisher ones.

我们必须仔细考虑get_context_data()。因为SingleObjectMixinListView 都会将Context 数据的context_object_name 下,我们必须显式确保Publisher 位于Context 数据中。ListView 将为我们添加合适的page_objpaginator ,只要我们记住调用super()

现在,我们可以编写一个新的PublisherDetail

  1. from django.views.generic import ListView
  2. from django.views.generic.detail import SingleObjectMixin
  3. from books.models import Publisher
  4. class PublisherDetail(SingleObjectMixin, ListView):
  5. paginate_by = 2
  6. template_name = "books/publisher_detail.html"
  7. def get(self, request, *args, **kwargs):
  8. self.object = self.get_object(queryset=Publisher.objects.all())
  9. return super(PublisherDetail, self).get(request, *args, **kwargs)
  10. def get_context_data(self, **kwargs):
  11. context = super(PublisherDetail, self).get_context_data(**kwargs)
  12. context['publisher'] = self.object
  13. return context
  14. def get_queryset(self):
  15. return self.object.book_set.all()

注意我们 在 get()方法里设置了self.object ,这样我们就可以在后面的 get_context_data()get_queryset()方法里再次用到它. 如果不设置 template_name, 那模板会指向默认的 ListView 所选择的模板, 也就是 "books/book_list.html",因为这个模板是书目的一个列表; 但ListView 对于该类继承了 SingleObjectMixin这个类是一无所知的,所以不会对使用Publisher来查看视图有任何反应.

paginate_by是每页显示几条数据的意思,这里设的比较小,是因为这样你就不用造一堆数据才能看到分页的效果了!下面是你想要的模板:

  1. {% extends "base.html" %}
  2. {% block content %}
  3. <h2>Publisher {{ publisher.name }}</h2>
  4. <ol>
  5. {% for book in page_obj %}
  6. <li>{{ book.title }}</li>
  7. {% endfor %}
  8. </ol>
  9. class="pagination">
  10. class="step-links">
  11. {% if page_obj.has_previous %}
  12. <a href="?page={{ page_obj.previous_page_number }}">previous</a>
  13. {% endif %}
  14. class="current">
  15. Page {{ page_obj.number }} of {{ paginator.num_pages }}.
  16. {% if page_obj.has_next %}
  17. <a href="?page={{ page_obj.next_page_number }}">next</a>
  18. {% endif %}
  19. {% endblock %}

避免让事情复杂化

通常情况下你只在需要相关功能时才会使用 TemplateResponseMixinSingleObjectMixin这两个类。如上所示,只要加点儿小心,你甚至可以把SingleObjectMixinListView结合在一起来使用. 但是这么搞可能会让事情变得有点复杂,作为一个好的原则:

提示:

你的视图扩展应该仅仅使用那些来自于同一组通用基类的view或者mixins。如: detail, list, editing 和 date. 例如:把 TemplateView (内建视图)和 MultipleObjectMixin (通用列表)整合在一起是极好的, 但是若想把SingleObjectMixin (generic detail) 和 MultipleObjectMixin (generic list)整合在一起就有麻烦啦!

To show what happens when you try to get more sophisticated, we show an example that sacrifices readability and maintainability when there is a simpler solution. 首先,我们来看一下如何把DetailViewFormMixin结合起来,实现 POST 一个 Django 表单 到相同URL,这样我们就可以用DetailView来显示具体对象了.

使用 FormMixin 与 DetailView

想想我们之前合用 ViewSingleObjectMixin 的例子. 我们想要记录用户对哪些作者感兴趣; 也就是说我们想让用户发表说为什么喜欢这些作者的信息。同样的,我们假设这些数据并没有存放在关系数据库里,而是存在另外一个奥妙之地(其实这里不用关心具体存放到了哪里)。

要实现这一点,自然而然就要设计一个?Form,让用户把相关信息通过浏览器发送到Django后台。 另外,我们要巧用REST方法,这样我们就可以用相同的URL来显示作者和捕捉来自用户的消息了。 让我们重写 AuthorDetailView 来实现它。

We’ll keep the GET handling from DetailView, although we’ll have to add a Form into the context data so we can render it in the template. We’ll also want to pull in form processing from FormMixin, and write a bit of code so that on POST the form gets called appropriately.

Note

We use FormMixin and implement post() ourselves rather than try to mix DetailView with FormView (which provides a suitable post() already) because both of the views implement get(), and things would get much more confusing.

Our new AuthorDetail looks like this:

  1. # CAUTION: you almost certainly do not want to do this.
  2. # It is provided as part of a discussion of problems you can
  3. # run into when combining different generic class-based view
  4. # functionality that is not designed to be used together.
  5. from django import forms
  6. from django.http import HttpResponseForbidden
  7. from django.core.urlresolvers import reverse
  8. from django.views.generic import DetailView
  9. from django.views.generic.edit import FormMixin
  10. from books.models import Author
  11. class AuthorInterestForm(forms.Form):
  12. message = forms.CharField()
  13. class AuthorDetail(FormMixin, DetailView):
  14. model = Author
  15. form_class = AuthorInterestForm
  16. def get_success_url(self):
  17. return reverse('author-detail', kwargs={'pk': self.object.pk})
  18. def get_context_data(self, **kwargs):
  19. context = super(AuthorDetail, self).get_context_data(**kwargs)
  20. context['form'] = self.get_form()
  21. return context
  22. def post(self, request, *args, **kwargs):
  23. if not request.user.is_authenticated():
  24. return HttpResponseForbidden()
  25. self.object = self.get_object()
  26. form = self.get_form()
  27. if form.is_valid():
  28. return self.form_valid(form)
  29. else:
  30. return self.form_invalid(form)
  31. def form_valid(self, form):
  32. # Here, we would record the user's interest using the message
  33. # passed in form.cleaned_data['message']
  34. return super(AuthorDetail, self).form_valid(form)

get_success_url() is just providing somewhere to redirect to, which gets used in the default implementation of form_valid(). We have to provide our own post() as noted earlier, and override get_context_data() to make the Form available in the context data.

优化方案

It should be obvious that the number of subtle interactions between FormMixin and DetailView is already testing our ability to manage things. 你不太可能会去想自己写这种类的。

In this case, it would be fairly easy to just write the post() method yourself, keeping DetailView as the only generic functionality, although writing Form handling code involves a lot of duplication.

Alternatively, it would still be easier than the above approach to have a separate view for processing the form, which could use FormView distinct from DetailView without concerns.

其他可选的方案

What we’re really trying to do here is to use two different class based views from the same URL. So why not do just that? We have a very clear division here: GET requests should get the DetailView (with the Form added to the context data), and POST requests should get the FormView. Let’s set up those views first.

The AuthorDisplay view is almost the same as when we first introduced AuthorDetail; we have to write our own get_context_data() to make the AuthorInterestForm available to the template. We’ll skip the get_object() override from before for clarity:

  1. from django.views.generic import DetailView
  2. from django import forms
  3. from books.models import Author
  4. class AuthorInterestForm(forms.Form):
  5. message = forms.CharField()
  6. class AuthorDisplay(DetailView):
  7. model = Author
  8. def get_context_data(self, **kwargs):
  9. context = super(AuthorDisplay, self).get_context_data(**kwargs)
  10. context['form'] = AuthorInterestForm()
  11. return context

?AuthorInterest是一个简单的 FormView, 但是我们不得不把SingleObjectMixin引入进来,这样我们才能定位我们评论的作者,并且我们还要记得设置template_name来确保form出错时使用?GET会渲染到?AuthorDisplay相同的模板 :

  1. from django.core.urlresolvers import reverse
  2. from django.http import HttpResponseForbidden
  3. from django.views.generic import FormView
  4. from django.views.generic.detail import SingleObjectMixin
  5. class AuthorInterest(SingleObjectMixin, FormView):
  6. template_name = 'books/author_detail.html'
  7. form_class = AuthorInterestForm
  8. model = Author
  9. def post(self, request, *args, **kwargs):
  10. if not request.user.is_authenticated():
  11. return HttpResponseForbidden()
  12. self.object = self.get_object()
  13. return super(AuthorInterest, self).post(request, *args, **kwargs)
  14. def get_success_url(self):
  15. return reverse('author-detail', kwargs={'pk': self.object.pk})

Finally we bring this together in a new AuthorDetail view. We already know that calling as_view() on a class-based view gives us something that behaves exactly like a function based view, so we can do that at the point we choose between the two subviews.

You can of course pass through keyword arguments to as_view() in the same way you would in your URLconf, such as if you wanted the AuthorInterest behavior to also appear at another URL but using a different template:

  1. from django.views.generic import View
  2. class AuthorDetail(View):
  3. def get(self, request, *args, **kwargs):
  4. view = AuthorDisplay.as_view()
  5. return view(request, *args, **kwargs)
  6. def post(self, request, *args, **kwargs):
  7. view = AuthorInterest.as_view()
  8. return view(request, *args, **kwargs)

This approach can also be used with any other generic class-based views or your own class-based views inheriting directly from View or TemplateView, as it keeps the different views as separate as possible.

返回HTML 以外的内容

基于类的视图在同一件事需要实现多次的时候非常有优势。假设你正在编写API,每个视图应该返回JSON 而不是渲染后的HTML。

我们可以创建一个Mixin 类来处理JSON 的转换,并将它用于所有的视图。

例如,一个简单的JSON Mixin 可能像这样:

  1. from django.http import JsonResponse
  2. class JSONResponseMixin(object):
  3. """
  4. A mixin that can be used to render a JSON response.
  5. """
  6. def render_to_json_response(self, context, **response_kwargs):
  7. """
  8. Returns a JSON response, transforming 'context' to make the payload.
  9. """
  10. return JsonResponse(
  11. self.get_data(context),
  12. **response_kwargs
  13. )
  14. def get_data(self, context):
  15. """
  16. Returns an object that will be serialized as JSON by json.dumps().
  17. """
  18. # Note: This is *EXTREMELY* naive; in reality, you'll need
  19. # to do much more complex handling to ensure that arbitrary
  20. # objects -- such as Django model instances or querysets
  21. # -- can be serialized as JSON.
  22. return context

查看序列化Django 对象 的文档,其中有如何正确转换Django 模型和查询集到JSON 的更多信息。

该Mixin 提供一个render_to_json_response() 方法,它与 render_to_response() 的参数相同。要使用它,我们只需要将它与TemplateView 组合,并覆盖render_to_response() 来调用render_to_json_response()

  1. from django.views.generic import TemplateView
  2. class JSONView(JSONResponseMixin, TemplateView):
  3. def render_to_response(self, context, **response_kwargs):
  4. return self.render_to_json_response(context, **response_kwargs)

同样地,我们可以将我们的Mixin 与某个通用的视图一起使用。我们可以实现自己的DetailView 版本,将JSONResponseMixindjango.views.generic.detail.BaseDetailView 组合– (the DetailView before template rendering behavior has been mixed in):

  1. from django.views.generic.detail import BaseDetailView
  2. class JSONDetailView(JSONResponseMixin, BaseDetailView):
  3. def render_to_response(self, context, **response_kwargs):
  4. return self.render_to_json_response(context, **response_kwargs)

这个视图可以和其它DetailView 一样使用,它们的行为完全相同 —— 除了响应的格式之外。

如果你想更进一步,你可以组合DetailView 的子类,它根据HTTP 请求的某个属性能够返回HTML 又能够返回JSON 内容,例如查询参数或HTTP 头部。这只需将JSONResponseMixinSingleObjectTemplateResponseMixin 组合,并覆盖render_to_response() 的实现以根据用户请求的响应类型进行正确的渲染:

  1. from django.views.generic.detail import SingleObjectTemplateResponseMixin
  2. class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
  3. def render_to_response(self, context):
  4. # Look for a 'format=json' GET argument
  5. if self.request.GET.get('format') == 'json':
  6. return self.render_to_json_response(context)
  7. else:
  8. return super(HybridDetailView, self).render_to_response(context)

由于Python 解析方法重载的方式,super(HybridDetailView, self).render_to_response(context) 调用将以调用 TemplateResponseMixinrender_to_response() 实现结束。